package com.clementheliou.cheliou.web.controller.gallery.technicaldatasheet;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.faces.event.ActionEvent;

import org.primefaces.model.LazyDataModel;
import org.primefaces.model.map.DefaultMapModel;
import org.primefaces.model.map.LatLng;
import org.primefaces.model.map.MapModel;
import org.primefaces.model.map.Marker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;

import com.clementheliou.cheliou.business.dataaccess.comment.CommentDataAccessService;
import com.clementheliou.cheliou.business.dataaccess.mark.MarkDataAccessService;
import com.clementheliou.cheliou.business.dataaccess.member.MemberDataAccessService;
import com.clementheliou.cheliou.business.dataaccess.photo.PhotoDataAccessService;
import com.clementheliou.cheliou.business.functional.mark.MarkFunctionalService;
import com.clementheliou.cheliou.business.functional.photo.PhotoFunctionalService;
import com.clementheliou.cheliou.dal.model.comment.Comment;
import com.clementheliou.cheliou.dal.model.mark.Mark;
import com.clementheliou.cheliou.dal.model.photo.AuthorsNote;
import com.clementheliou.cheliou.dal.model.photo.Photo;
import com.clementheliou.cheliou.util.math.average.AverageMark;
import com.clementheliou.cheliou.util.time.TimeUtils;
import com.clementheliou.cheliou.web.controller.common.navigation.NavigationBean;
import com.clementheliou.cheliou.web.controller.gallery.technicaldatasheet.comment.model.CommentLazyDataModel;
import com.clementheliou.cheliou.web.controller.user.UserBean;

/**
 * Controller managing the technical data sheet view. It's scoped to an HTTP session lifecycle.
 * 
 * @author Clément HELIOU (clement.heliou@gmail.com)
 * @see {@link Controller};
 * @see {@link InitializingBean};
 * @see {@link Scope};
 * @see {@link Serializable}.
 * @since 1.0
 */
@Controller
@Scope("session")
public class TechnicalDataSheetBean implements InitializingBean, Serializable {

	/**
	 * Logger for this class.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Logger};
	 * @see {@link LoggerFactory}.
	 * @since 1.0
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(TechnicalDataSheetBean.class);

	/**
	 * UID for serialization.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private static final long serialVersionUID = -7146613877444104477L;

	/**
	 * Data access service about the comments.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired}.
	 * @since 1.0
	 */
	@Autowired
	private transient CommentDataAccessService commentService;

	/**
	 * Data access service about the marks.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link MarkDataAccessService}.
	 * @since 1.0
	 */
	@Autowired
	private transient MarkDataAccessService markService;

	/**
	 * Functional service about the marks.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	@Autowired
	private transient MarkFunctionalService markFunctionalService;

	/**
	 * Data access service about the members.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link MarkDataAccessService}.
	 * @since 1.0
	 */
	@Autowired
	private transient MemberDataAccessService memberService;

	/**
	 * Data access service about the photos;
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link PhotoDataAccessService}.
	 * @since 1.0
	 */
	@Autowired
	private transient PhotoDataAccessService photoService;

	/**
	 * Functional service about the photos;
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link PhotoFunctionalService}.
	 * @since 1.0
	 */
	@Autowired
	private transient PhotoFunctionalService photoFunctionalService;

	/**
	 * Bean used to navigate between the application's views.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link NavigationBean}.
	 * @since 1.0
	 */
	@Autowired
	private transient NavigationBean navigationBean;

	/**
	 * Bean used to access the user's data;
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Autowired};
	 * @see {@link UserBean}.
	 * @since 1.0
	 */
	@Autowired
	private transient UserBean userBean;

	/**
	 * {@link LazyDataModel} binded to the comments data grid.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	@Autowired
	private CommentLazyDataModel commentsModel;

	/**
	 * The identifier of the photo to be loading when the {@link #onSheetAccess(ActionEvent)} action
	 * listener is called. It will be set by the view through the
	 * <tt>f:setPropertyActionListener</tt> JSF tag.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private Integer currentPhotoId;

	/**
	 * The mark's value given by the connected user (if any) to the current photo.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private Integer currentMarkValue;

	/**
	 * Cache for the average marks that have been loaded during the HTTP session lifecycle. This one
	 * will be removed during the session destroying. Each item of the map will be cleaned when the
	 * user marks the related photo.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private Map<Integer, AverageMark> averageMarkCacheMap = new HashMap<Integer, AverageMark>();

	/**
	 * Cache for the marks that have been loaded during the HTTP session lifecycle. This one will be
	 * removed during the session destroying. Each item of the map will be cleaned when the user
	 * marks the related photo.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private Map<Integer, Mark> markCacheMap = new HashMap<Integer, Mark>();

	/**
	 * Cache for photographies that have been loaded during the HTTP session lifecycle. This one
	 * will be removed during the session destroying.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private Map<Integer, Photo> photosCacheMap = new HashMap<Integer, Photo>();

	/**
	 * Cache for photographies paths that have been loaded during the HTTP session lifecycle. This
	 * one will be removed during the session destroying.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private Map<Integer, String> photosPathsCacheMap = new HashMap<Integer, String>();

	/**
	 * {@link MapModel} component related to the current photo.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private MapModel currentMapModel;

	/**
	 * The photo the view is about.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @see {@link Photo}.
	 * @since 1.0
	 */
	private Photo currentPhoto;

	/**
	 * Content of the comment to be added by the connected user.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private String commentContentToBeAdded;

	/**
	 * The path of the current photo to be used to display the biggest available version. It must be
	 * synchronized with the {@link #currentPhoto} value.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	private String currentPhotoPath;

	/**
	 * Method called when the user comments the current photo. It persists the given comment and
	 * reinits the {@link #commentContentToBeAdded} field to allow a new one.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return <tt>null</tt> because we stay on the same page.
	 * @since 1.0
	 */
	public String addComment() {
		LOGGER.debug("Add a comment for the photo {}.", currentPhotoId);

		final Comment commentToBeAdded = new Comment();
		commentToBeAdded.setAdditionDate(TimeUtils.now());
		commentToBeAdded.setAuthor(memberService.getMemberByEMail(userBean.getCurrentUser().getUsername()));
		commentToBeAdded.setCommentedPhoto(currentPhoto);
		commentToBeAdded.setContent(commentContentToBeAdded);

		commentService.addComment(commentToBeAdded);
		commentContentToBeAdded = null;

		return null;
	}

	/**
	 * Action method that will perform the {@link #currentPhoto} loading just before the view
	 * displaying. The photo to be loaded is retrieved through the {@link #currentPhotoId}, that
	 * contains the photo's identifier. If this one is null, the loading won't be performed and the
	 * view should display an error message. The page to be displayed is retrieved from the
	 * {@link NavigationBean}.
	 * <p>
	 * The value of {@link #currentPhotoPath} is synchronized with the {@link #currentPhoto} one.
	 * Please not that caches are used to improve the loading time ({@link #photosCacheMap} and
	 * {@link #photosPathsCacheMap}). If the photo id is present in the {@link #photosCacheMap}, it
	 * won't be reloaded from the service neither its paths. Caches are cleaned at the end of the
	 * HTTP session. By this way, we ensure one single load per session for each technical data
	 * sheet.
	 * </p>
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return the view to be displayed.
	 * @since 1.0
	 */
	public String loadPhoto() {
		if (currentPhotoId != null) {
			final Photo cachedValue = photosCacheMap.get(currentPhotoId);

			if (null == cachedValue) {
				LOGGER.debug("Put the photo with {} id and its path in the cache maps.", currentPhotoId);
				photosCacheMap.put(currentPhotoId, photoService.getPhotoById(currentPhotoId));
				photosPathsCacheMap.put(currentPhotoId, photoFunctionalService.getPhotoPathToUse(userBean.getCurrentUser().getScreenSize(), photosCacheMap.get(currentPhotoId)));
			}

			LOGGER.debug("Get the photo with {} id and its path from the cache maps.", currentPhotoId);
			commentsModel.setPhotoId(currentPhotoId);
			currentPhoto = photosCacheMap.get(currentPhotoId);
			currentPhotoPath = photosPathsCacheMap.get(currentPhotoId);
			currentMapModel = new DefaultMapModel();
			currentMapModel.addOverlay(new Marker(new LatLng(currentPhoto.getLatitude().doubleValue(), currentPhoto.getLongitude().doubleValue())));
		}

		return navigationBean.getAccessTechnicalDataSheetPage();
	}

	/**
	 * Method called when the connected user marks the current photo. It persists the given mark and
	 * cleans the caches (marks and averages marks) to allow the view updates.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return <tt>null</tt> because we stay on the same page.
	 * @since 1.0
	 */
	public String addMark() {
		LOGGER.debug("Add the mark {} for the photo {}.", currentMarkValue, currentPhotoId);

		// 1. Add the mark.
		markService.addMark(markCacheMap.get(currentPhotoId));

		// 2. Clean the caches values.
		averageMarkCacheMap.remove(currentPhotoId);

		return null;
	}

	/**
	 * Gets the localized author's note related to the current photo.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return the localized author's note for the current photo if it exists, <tt>null</tt>
	 *         otherwise.
	 * @since 1.0
	 */
	public AuthorsNote getAuthorsNote() {
		final Locale currentLocale = userBean.getCurrentLocale();

		for (AuthorsNote note : currentPhoto.getAuthorsNotes()) {
			if (note.getLocale().getLocale().equals(currentLocale)) {
				LOGGER.debug("Author's note found for the photo {} and the locale {}.", currentPhotoId, currentLocale);
				return note;
			}
		}

		LOGGER.debug("No author's note for the photo {} and the locale {}.", currentPhotoId, currentLocale);
		return null;
	}

	/**
	 * Gets the average mark related to the current photo. If this one has already been loaded, it
	 * will retrieved from the {@link #averageMarkCacheMap}.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return the average mark related to the current photo.
	 * @since 1.0
	 */
	public AverageMark getCurrentAverageMark() {
		if (!averageMarkCacheMap.containsKey(currentPhotoId)) {
			LOGGER.debug("Put the average mark in the cache for the photo {}.", currentPhotoId);
			averageMarkCacheMap.put(currentPhotoId, markFunctionalService.getAverageMarkOfPhoto(currentPhotoId));
		}

		LOGGER.debug("Get the average mark of the photo {} from the cache.", currentPhotoId);
		return averageMarkCacheMap.get(currentPhotoId);
	}

	/**
	 * Gets the mark given by the connected user to the current photo. If this one has already been
	 * loaded, it will retrieved from the {@link #markCacheMap}.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @return
	 * @since 1.0
	 */
	public Integer getCurrentMarkValue() {
		if (!markCacheMap.containsKey(currentPhotoId)) {
			LOGGER.debug("Put the mark in the cache for the photo {}.", currentPhotoId);
			markCacheMap.put(currentPhotoId, markService.getMarkOfMember(currentPhotoId, userBean.getCurrentUser().getMemberId()));
		}

		LOGGER.debug("Get the mark of the photo {} from the cache.", currentPhotoId);
		currentMarkValue = (markCacheMap.get(currentPhotoId) != null) ? markCacheMap.get(currentPhotoId).getRank() : null;
		return currentMarkValue;
	}

	/**
	 * Sets the mark given by the connected user to the current photo. This setter updates the cache
	 * map.
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @param currentMarkValue the current mark's value.
	 * @since 1.0
	 */
	public void setCurrentMarkValue(Integer currentMarkValue) {
		final Mark markToBeAdded = new Mark();
		markToBeAdded.setAdditionDate(TimeUtils.now());
		markToBeAdded.setAuthor(memberService.getMemberByEMail(userBean.getCurrentUser().getUsername()));
		markToBeAdded.setMarkedPhoto(currentPhoto);
		markToBeAdded.setRank(currentMarkValue);

		markCacheMap.put(currentPhotoId, markToBeAdded);
	}

	/**
	 * <i>{@inheritDoc}</i>
	 * 
	 * @author Clément HELIOU (clement.heliou@gmail.com)
	 * @since 1.0
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		Assert.notNull(navigationBean, "navigationBean must not be null at this time.");
		Assert.notNull(photoService, "photoService must not be null at this time.");
	}

	/*
	 * Getters & setters.
	 */

	public Photo getCurrentPhoto() {
		return currentPhoto;
	}

	public void setCurrentPhoto(Photo currentPhoto) {
		this.currentPhoto = currentPhoto;
	}

	public Integer getCurrentPhotoId() {
		return currentPhotoId;
	}

	public void setCurrentPhotoId(Integer currentPhotoId) {
		this.currentPhotoId = currentPhotoId;
	}

	public MapModel getCurrentMapModel() {
		return currentMapModel;
	}

	public Mark getCurrentMark() {
		return markCacheMap.get(currentPhotoId);
	}

	public CommentLazyDataModel getCommentsModel() {
		return commentsModel;
	}

	public String getCommentContentToBeAdded() {
		return commentContentToBeAdded;
	}

	public void setCommentContentToBeAdded(String commentContentToBeAdded) {
		this.commentContentToBeAdded = commentContentToBeAdded;
	}

	public String getCurrentPhotoPath() {
		return currentPhotoPath;
	}

}
